SpringAI - ToolCalling(二)
函数工具调用
函数即工具
- 在方法即工具中有一项限制是参数和返回类型不得是函数式类型(如
Function
、Supplier
、Consumer
)。 - SpringAI为函数式类型单独提供了构建工具的方式。
使用 FunctionToolCallback
可以通过
FunctionToolCallback
将Java中的函数式类型(Function
、Supplier
、Consumer
或BiFunction
)构建成工具。使用
FunctionToolCallback.Builder
来构建FunctionToolCallback
实例。可以构建的属性如下:String name
:工具名称。String description
:工具描述。Type inputType
:函数输入类型。必须输入。如果是无输入参数的工具使用Void.class
。String inputSchema
:输入参数的Json Schema
。这里非必须,默认按inputType
生成。ToolMetadata toolMetadata
:额外配置,通过ToolMetadata.Builder
类构建。BiFunction<I, ToolContext, O> toolFunction
:用于工具调用的函数类型对象。ToolCallResultConverter toolCallResultConverter
:工具调用结果的转换器。
构建的属性整体与
MethodToolCallback.Builder
类似。下面模拟一个将美元兑换成RMB的工具案例。这里模拟一个RMB的单位,创建一个单位枚举,包含分/角/元。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public enum Unit {
F(100),
J(10),
Y(1)
;
private int multiple;
public BigDecimal getUnitMultiple() {
return BigDecimal.valueOf(this.getMultiple());
}
}然后创建输入、输出的recode类。单位非必须项,默认:分。
1
2
3
4
5
6
7
8
9
10
11
12
13public record Request(@ToolParam(description = "需要转换成人民币的美元数量。") BigDecimal dollar,
Unit unit) {
public Request {
if(Objects.isNull(unit)) {
unit = Unit.F;
}
}
}
public record Response(BigDecimal rmb) {
}创建一个工具类实现
Function<I, O>
。Request/Response
是内部类。1
2
3
4
5
6
7
8
9
10
11
12
public class FuncDollarToRmbTools implements Function<FuncDollarToRmbTools.Request, FuncDollarToRmbTools.Response> {
private final BigDecimal rate = BigDecimal.valueOf(7.17);
public Response apply(Request request) {
log.info("\n美元转换成人民币,参数 -> {}。", ConvertorUtils.toJsonString(request));
return new Response(request.dollar()
.multiply(rate)
.multiply(request.unit().getUnitMultiple())
.setScale(2));
}
}使用
FunctionToolCallback.Builder
构建实例1
2
3
4
5
6
7
8FunctionToolCallback<FuncDollarToRmbTools.Request, FuncDollarToRmbTools.Response> toolCallback = FunctionToolCallback
// 名称和Function工具对象。
.builder("FuncDollarToRmbTools", new FuncDollarToRmbTools())
// 描述
.description("可以将美元转换成人名币的计算器,需要提供人名币数量,以及需要转换成的单位(默认:分)。")
// 输入类型
.inputType(FuncDollarToRmbTools.Request.class)
.build();是用
.toolCallbacks()
添加工具。这里调用两次,一次不说明单位,一次指定单位。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 不说明单位
String content = toolsClient.prompt()
.user("15美元可以兑换多少人名币?")
.toolCallbacks(toolCallback)
.call()
.content();
log.info("\n[funcToolsExample] content -> \n{}", content);
// 指定单位
content = toolsClient.prompt()
.user("15美元可以兑换多少角人名币?")
.toolCallbacks(toolCallback)
.call()
.content();
log.info("\n[funcToolsExample] unit(角) content -> \n{}", content);结果输出。两个都掉用了工具,指定单位时单位也换成了枚举中的J。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
172025-07-11T15:47:13.895+08:00 INFO 218 --- [spring-ai-example] [ main] c.s.a.e.tools.two.FuncDollarToRmbTools :
美元转换成人民币,参数 -> {
"dollar" : 15,
"unit" : "F"
}。
2025-07-11T15:47:18.514+08:00 INFO 218 --- [spring-ai-example] [ main] c.s.a.e.tools.two.FuncToolsExample :
[funcToolsExample] content ->
15美元可以兑换107.55元人民币(以分为单位计算为10755分)。
2025-07-11T15:47:23.503+08:00 INFO 218 --- [spring-ai-example] [ main] c.s.a.e.tools.two.FuncDollarToRmbTools :
美元转换成人民币,参数 -> {
"dollar" : 15,
"unit" : "J"
}。
2025-07-11T15:47:29.850+08:00 INFO 218 --- [spring-ai-example] [ main] c.s.a.e.tools.two.FuncToolsExample :
[funcToolsExample] unit(角) content ->
15美元可以兑换1075.5角人民币。
使用
Supplier
无输入参数的工具1
2
3
4
5
6
7
8
9
10String content = toolsClient.prompt()
.user("下个月5号是星期几?")
.toolCallbacks(FunctionToolCallback
.builder("getDatetime", () -> LocalDateTime.now().toString())
.description("可以获取当前时间。")
// 使用 Void.class
.inputType(Void.class)
.build())
.call()
.content();
将工具定义成Spring Bean
上面是通过手动构建将工具实例添加到
ChatClient
,也可以通过将工具定义为Spring Bean
添加到ChatClient
。- 默认使用
Bean
的名称作为工具名称。 - 可以通过
@Description
注解来配置工具描述。
- 默认使用
使用
@Configuration
的方式来定义Bean`,案例如下:这里定义两个工具
Bean
,一个Function
,一个Supplier
,且都使用的表达式编程。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class BeanToolsConfiguration {
public Function<FuncDollarToRmbTools.Request, FuncDollarToRmbTools.Response> beanRmbTools() {
return request -> new FuncDollarToRmbTools.Response(request.dollar()
.multiply(BigDecimal.valueOf(7.17))
.multiply(request.unit().getUnitMultiple())
.setScale(2));
}
public Supplier<String> beanDataTools() {
return () -> LocalDateTime.now().toString();
}
}先看看
Supplier
的工具调用。添加工具使用.toolNames()
添加Bean
名称就可以了。1
2
3
4
5
6
7
8
9public void beanDataToolsExample() {
String content = toolsClient.prompt()
.user("下个月5号是星期几?")
// 直接添加Bean(工具)名称就行
.toolNames("beanDataTools")
.call()
.content();
log.info("\n[simpleFuncToolsExample] content -> \n{}", content);
}调用结果。这个肯定是获取了当前时间的,一般AI是无法知道当前时间的。
1
2
3
4
52025-07-11T16:08:15.276+08:00 INFO 716 --- [spring-ai-example] [ main] c.s.a.e.tools.two.BeanFuncToolsExample :
[simpleFuncToolsExample] content ->
当前时间是2025年7月11日。下个月5号是2025年8月5日。我们需要计算2025年8月5日是星期几。
2025年8月5日是星期二。Function
的工具调用1
2
3
4
5
6
7
8public void beanRmbToolsExample() {
String content = toolsClient.prompt()
.user("33美元可以兑换多少元人名币?")
.toolNames("beanRmbTools")
.call()
.content();
log.info("\n[funcToolsExample] unit(元) content -> \n{}", content);
}结果。是正常调用了的。
1
2
32025-07-11T16:11:12.065+08:00 INFO 794 --- [spring-ai-example] [ main] c.s.a.e.tools.two.BeanFuncToolsExample :
[funcToolsExample] unit(元) content ->
33美元可以兑换236.61元人民币。可以将方法即工具的Java实例对象定义成
Bean
,然后使用Bean Name
添加工具吗?先说结论,是不可以的。
因为Spring默认将从容器中取到的
Bean
构建成FunctionToolCallback
。且如果不是函数式类型抛出异常。
总结
- 函数式工具使用上相对方便、快捷,特别是定义成
bean
的方式,配合使用表达式编程,是较为高效的一种方式。 - 但是函数式工具的限制更多,不支持的类型相对更多:
- 基本类型
- 集合类型(
List
、Map
、Array
、Set
) Optional
- 异步类型(如
CompletableFuture
、Future
) - 响应式类型(如
Flow
、Mono
、Flux
)
- 不支持基本类型应该是函数式类型输入输出都是泛型,不支持基本类型,可以用包装类。
- 集合类型类型不支持推测也是泛型无法识别集合中实际类型无法生成
Schema
,主要应该是定义成bean
的方式。 - 选择使用函数式工具还是方法即工具,可以从限制上来考虑,当然还要结合编程习惯以及团队的编程规范综合考虑。
最后
- 不论基于方法的工具还是函数式编程,只是编程方式不同而已,其本质理论上还是一样的。
- 下一篇继续学习记录工具调用SpringAI提供的一些特性。
- 所有案例的源码,都会提交在GitHub上。包:
com.spring.ai.example.tools.two